Shadow Door User's Manual

Copyright 2003 by Robert Zubek and Phillip Saltzman, Northwestern University
shadow-door-dev@cs.northwestern.edu

 

Using the LISP API

Setting up Lisp communication involves two things: both the NPC and Lisp need to be configured to communicate through the Shadow Door.

We will examine how to do this using the Allegro Common Lisp development environment - see previous chapter  for information on how to obtain a free evaluation copy and how to run it with NWServer.

 

Game Side: Setting up the NPC

Suppose you already have a game module with an NPC that you would like to be controlled externally. We will need to set up the NPC to talk through Shadow Door. Open your module in the Aurora Toolbox and:

  1. Go to File | Import, select shadowdoor.erf - this imports the script code
  2. Go to Edit | Module Properties, then the Events tab. Under OnModuleLoad select sd_on_load
  3. Create a new creature or select an existing NPC, then open its Properties dialog. Under the Scripts tab:

And that's all we need inside the module!

 

Lisp Side: Setting up and Using the API

All right, so now we're going to explore how to control an NPC from within Lisp. I'm assuming you have some Lisp familiarity at this point, but step-by-step examples are also provided.

The basic commands for Shadow Door are:

The NPC reports its observations of world events, which can be retrieved via (sd-read-sexp) as described above. The observations are simple lists of strings. The first element is the type of observation, and remaining ones contain additional information. The sd-utilities.lisp file also contains functions for recognizing received observations and extracting the extra info:


Example

Let's see how to use these functions in vivo. The following is an annotated transcript of using Shadow Door interactively from Lisp.

First, we start NWNX2 Shadow Door and load the Eliza module as before; then start the game and join the module so that we're again in the same room as Eliza. Now we switch back to Windows (without shutting down the game), and start Lisp.

First, let's change packages, enter the directory, and compile and load the Lisp files. Notice I keep my game on the E: drive, in an odd directory - you should replace that with your own path:

cg-user(14): (in-package common-lisp-user) 
#<The common-lisp-user package>

cl-user(15): :cd e:/neverwinternights/nwn/shadow door/lisp sources
e:\neverwinternights\nwn\shadow door\lisp sources\

cl-user(16): :cl sd-allegro-socket-interface.lisp
;;; Compiling file sd-allegro-socket-interface.lisp
; While compiling (:top-level-form "sd-allegro-socket-interface.lisp" 464):
;;; Writing fasl file sd-allegro-socket-interface.fasl
;;; Fasl write complete
; Fast loading
;    e:\neverwinternights\nwn\shadow door\lisp sources\sd-allegro-socket-interface.fasl

cl-user(17): :cl sd-utilities.lisp
;;; Compiling file sd-utilities.lisp 
;;; Writing fasl file sd-utilities.fasl 
;;; Fasl write complete 
; Fast loading e:\neverwinternights\nwn\shadow door\lisp sources\sd-utilities.fasl

The game server address is stored in the **sd variable, which is a connection struct, under the server-name slot. It's set to "localhost" by default, but we can change it by doing:

cl-user(20): (setf (connection-server-name **sd) "129.105.100.165")
"129.105.100.165"

Now we let Lisp connect to the server, and send a simple command to say something and play an animation:

cl-user(24): (sd-connect)
#<multivalent stream socket connected from localhost/3039 
to localhost/1890 @#x21016302>
cl-user(25): (sd-send-speak "greetings")
nil
cl-user(26): (progn 
               (sleep 12) 
               (sd-send-animate animation-oneshot-salute) 
               (sleep 1.0)
               (sd-send-speak "how are you sir?"))
nil

We quickly switch back to the game, just in time to observe the salute and the question being performed. In response, we type a quick reply in the chat window, and switch back to Lisp. Let's retrieve the utterance spoken in the game, save it in a variable and examine it, then direct the agent to approach some object in the game and say another string:

cl-user(27): (setf incoming (sd-read-sexp))
("speech" "rob" "says" "i'm fine, thank you")

cl-user(28): (sd-speech? incoming)
t
cl-user(29): (sd-speech-text incoming)
"i'm fine, thank you"
cl-user(30): (sd-speech-speaker incoming)
"rob"

cl-user(31): (sd-send-moveto "chest1")
nil
cl-user(32): (sd-send-speak "can you open this chest?")
nil

Switching back to the game, we can see the agent moved closer to the object and said the string. We now attack the agent, and then switch to Lisp, to retrieve observation about the attack, and attempt a counter-attack:

cl-user(36): (setf incoming (sd-read-sexp))
("attacked-by" "rob")
cl-user(37): (sd-attack? incoming)
t
cl-user(38): (sd-send-attack (sd-attacker-name incoming))
nil

At which point the agent, being much weaker, dies. We disconnect from the game.

cl-user(39): (sd-disconnect)
nil

And there we are.